package com.inspinia.base.dao.pageable;

import com.inspinia.base.pagination.PageQueryVo;
import com.inspinia.base.pagination.Pager;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 分页插件，使用 Pageable 和 Page 对象作为输入和输出。
 * 只要返回值是Pager返回值，则自动进行分页。
 * <p>
 * 方言类和部分逻辑来自： https://github.com/miemiedev/mybatis-paginator
 *
 * @author zany@buzheng.org
 */
@Intercepts({@Signature(type = Executor.class, method = "query", args =
        {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MybatisPageableInterceptor implements Interceptor {

    private static Logger logger = LoggerFactory.getLogger(MybatisPageableInterceptor.class);

    private static final Map<String, Boolean> pageMethodMap = new HashMap<>(); //分页方法的map集合。键是MappedStatement的id，值表示是否该方法是分页方法。


    private Dialect dialect;
    static int MAPPED_STATEMENT_INDEX = 0;
    static int PARAMETER_INDEX = 1;
    static int ROWBOUNDS_INDEX = 2;
    static int RESULT_HANDLER_INDEX = 3;

    @Override
    public Object intercept(Invocation inv) throws Throwable {


        final Object[] queryArgs = inv.getArgs();

        //判断是否是需要分页的方法
        final MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
        // 查找方法参数中的 分页请求对象
        final Object parameter = queryArgs[PARAMETER_INDEX];
        boolean pageMethod = isPageMethod(ms, parameter);
        if (pageMethod) {
            Pageable pageRequest = this.findPageableObject(parameter);

            // 如果需要分页
            if (pageRequest != null) {
                final BoundSql boundSql = ms.getBoundSql(parameter);

                // 删除尾部的 ';'
                String sql = boundSql.getSql().trim().replaceAll(";$", "");

                if (pageRequest.getKeyWord() != null && !"".equals(pageRequest.getKeyWord())) {
                    String keyWord = StringEscapeUtils.unescapeHtml4(pageRequest.getKeyWord());
                    if (keyWord.contains("'")){
                        keyWord = keyWord.replace("\'","\\'");
                    }
                    String sql1 = sql.substring(6, getMax(sql.lastIndexOf("from"), sql.lastIndexOf("FROM"))).replace("\n", "");//去除select和where后的sql
                    String sqllast = sql.substring(getMax(sql.lastIndexOf("from"), sql.lastIndexOf("FROM")), sql.length());
                    String[] concatP = getConcat(sql1);
                    if (sql1.contains("concat")) {
                        sql1 = deleConcat(sql1);
                    }
                    String[] fields = sql1.split(",");
                    for (int i = 0; i < fields.length; i++) {
                        fields[i] = fields[i].trim();
                        if ((fields[i].contains("FROM") || fields[i].contains("from")) && !(fields[i].contains("SELECT") || fields[i].contains("select"))) {
                            if (fields[i].contains("FROM")) {
                                fields[i] = fields[i].substring(0, fields[i].indexOf("FROM"));
                            } else if (fields[i].contains("from")) {
                                fields[i] = fields[i].substring(0, fields[i].indexOf("from"));
                            }
                        }
                        if ((fields[i].contains("(") && fields[i].contains(")"))) {
                            fields[i] = fields[i].substring(fields[i].lastIndexOf("("), fields[i].lastIndexOf(")") + 1);
                        } else if (fields[i].contains("as ")) {
                            fields[i] = fields[i].substring(0, fields[i].lastIndexOf("as "));
                        } else if (fields[i].contains("AS ")) {
                            fields[i] = fields[i].substring(0, fields[i].lastIndexOf("AS "));
                        } else if (fields[i].contains(" ")) {
                            fields[i] = fields[i].replace("distinct","").trim();
                        }
                    }
                    String ss = "";
                    int concatLen = 0;
                    if (concatP != null && concatP.length >= 0) {
                        concatLen = concatP.length;
                    }
                    String[] num = new String[fields.length + concatLen];
                    for (int i = 0; i < fields.length; i++) {
                        num[i] = fields[i];
                    }
                    if (concatP != null && concatP.length > 0) {
                        for (int i = 0; i < concatP.length; i++) {
                            num[i + fields.length] = concatP[i];
                        }
                    }

                    if (sqllast.contains("where") || sqllast.contains("WHERE")) {
                        ss = "AND ( ";
                    } else {
                        ss = "where 1=1 AND ( ";
                    }
                    for (int i = 0; i < num.length; i++) {
                        if (num[i] != null && !"".equals(num[i])) {

                            if (num[i].contains("time")) {
                                Pattern pattern = Pattern.compile("[0-9]*");
                                Matcher isNum = pattern.matcher(keyWord);
                                if (isNum.matches()) {
                                    if (i == num.length - 1) {
                                        ss = ss + num[i] + " like CONCAT('%','" + keyWord + "' ,'%') )";
                                    } else {
                                        ss = ss + num[i] + " like CONCAT('%','" + keyWord + "','%') or ";
                                    }
                                } else {
                                    continue;
                                }
                            } else {
                                if (i == num.length - 1) {
                                    ss = ss + num[i] + " like CONCAT('%','" + keyWord + "','%') )";
                                } else {
                                    ss = ss + num[i] + " like CONCAT('%','" + keyWord + "','%') or ";
                                }
                            }

                        }
                    }


                    int groupCount = 0;
                    if (sql.contains("group by")) {
                        groupCount = sql.lastIndexOf("group by");
                    } else if (sql.contains("GROUP BY")) {
                        groupCount = sql.lastIndexOf("GROUP BY");
                    }
                    int orderCount = 0;
                    if (sql.contains("order by")) {
                        orderCount = sql.lastIndexOf("order by");
                    } else if (sql.contains("ORDER BY")) {
                        orderCount = sql.lastIndexOf("ORDER BY");
                    }
                    if (orderCount > 0 && groupCount > 0) {
                        String sql2 = sql.substring(0, getMin(orderCount, groupCount));
                        String sql3 = sql.substring(getMin(orderCount, groupCount), sql.length());
                        sql2 = sql2 + ss;
                        sql = sql2 + sql3;
                    } else if (orderCount > 0 && groupCount == 0) {
                        String sql2 = sql.substring(0, orderCount);
                        String sql3 = sql.substring(orderCount, sql.length());
                        sql2 = sql2 + ss;
                        sql = sql2 + sql3;
                    } else if (orderCount == 0 && groupCount > 0) {
                        String sql2 = sql.substring(0, groupCount);
                        String sql3 = sql.substring(groupCount, sql.length());
                        sql2 = sql2 + ss;
                        sql = sql2 + sql3;
                    } else {
                        sql = sql + ss;
                    }

                }


                // 1. 搞定总记录数（如果需要的话）
                int total = 0;
                if (pageRequest.getCountable()) {
                    total = this.queryTotal(sql, ms, boundSql);
                }

                // 2. 搞定limit 查询
                // 2.1 获取分页SQL，并完成参数准备
                String limitSql = dialect.getLimitString(sql, pageRequest.getOffset(), pageRequest.getPageSize(), pageRequest.getOrderBy());

                queryArgs[ROWBOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
                queryArgs[MAPPED_STATEMENT_INDEX] = copyFromNewSql(ms, boundSql, limitSql);

                // 2.2 继续执行剩余步骤，获取查询结果
                Object ret = inv.proceed();

                // 3. 组成分页对象
                Pager<?> pi = new Pager<>(pageRequest.getPageNumber(), pageRequest.getPageSize(), total, (List<Object>) ret);

                // 4. MyBatis 需要返回一个List对象，这里只是满足MyBatis而作的临时包装
                List<Pager<?>> tmp = new ArrayList(1);
                tmp.add(pi);
                return tmp;
            }
        }
        return inv.proceed();

    }

    private int getMax(int par1, int par2) {
        if (par1 > par2) {
            return par1;
        } else {
            return par2;
        }

    }

    private int getMin(int par1, int par2) {
        if (par1 > par2) {
            return par2;
        } else {
            return par1;
        }

    }

    public String deleConcat(String sql) {
        if (sql != null) {
            String sql1 = sql.substring(0, sql.indexOf("concat"));
            String sql2 = sql.substring(sql.indexOf("concat"), sql.length());
            sql2 = sql2.substring(sql2.indexOf(")") + 1, sql2.length());
            if (sql2.contains(",")) {
                sql2 = sql2.substring(sql2.indexOf(","), sql2.length());
            } else {
                sql2 = "";
            }
            if (sql2 == "") {
                sql = sql1;
            } else {
                sql = sql1 + sql2;
            }
            if (sql.contains("concat")) {
                sql = deleConcat(sql);
            }
            return sql;
        }
        return null;

    }

    public String[] getConcat(String sql) {
        if (sql != null && sql.contains("concat")) {
            String[] split = sql.split("concat");
            String[] concatS = new String[split.length - 1];
            for (int i = 1; i < split.length; i++) {
                concatS[i - 1] = "concat" + split[i].substring(0, split[i].indexOf(")") + 1) +")";
            }
            return concatS;
        }
        return null;


    }


    /**
     * 判断方法是否需要分页
     *
     * @param ms
     * @param parameter
     * @return
     */
    private boolean isPageMethod(MappedStatement ms, Object parameter) {
        //如果返回值是Pager，则表示需要返回值。
        String statementId = ms.getId();
        Boolean isPageMethod = pageMethodMap.get(statementId);
        if (isPageMethod == null) {

            //通过反射查看方法的返回值是否是Pager。
            int last = statementId.lastIndexOf(".");
            String clazz = statementId.substring(0, last);
            String method = statementId.substring(last + 1, statementId.length());
            try {
                Class<?> aClass = Class.forName(clazz);

                //todo 原来想通过参数确定是什么函数。因为函数有重载的情况，光一个函数名无法确定函数。但是这里无法确定参数是包装类或是实现类，因此目前直接找第一个函数名称匹配的函数的返回值。
                Method[] methods = aClass.getMethods();
                Method invokeMethod = null;
                for (int i = 0; i < methods.length; i++) {
                    Method m = methods[i];
                    if (m.getName().equals(method)) {
                        invokeMethod = m;
                        break;
                    }
                }

                if (null != invokeMethod) {
                    //找到函数的返回值
                    Class<?> returnType = invokeMethod.getReturnType();

                    //根据返回值是否是Pager来判断是否是分页函数，然后将结果放入到Map中做缓存
                    if (returnType.isAssignableFrom(Pager.class)) {
                        pageMethodMap.put(statementId, true);
                        return true;
                    } else {
                        pageMethodMap.put(statementId, false);
                    }
                }
            } catch (ClassNotFoundException e) {
                logger.error("找不到分页Mapper类", e);
            }
        }
        return isPageMethod == null ? false : isPageMethod;
    }

    /**
     * 在方法参数中查找 分页请求对象
     *
     * @param params Mapper接口方法中的参数对象
     * @return
     */
    private Pageable findPageableObject(Object params) {

        if (params == null) {
            return null;
        }
        //如果参数是以对象方式传进来的。
        if (PageQueryVo.class.isAssignableFrom(params.getClass())) {
            PageQueryVo vo = (PageQueryVo) params;
            return new Pageable(vo.getPageNo(), vo.getPageSize(), vo.getSortBy(), vo.getKeyWord());
        } else if (params instanceof MapperMethod.ParamMap) {
            //如果是多个参数的，才进行查找参数中是否包含分页参数
            MapperMethod.ParamMap<Object> paramMap = (MapperMethod.ParamMap<Object>) params;
            if (paramMap.containsKey("pageNo")) {
                //todo 以后这里的分页大小和当前页可以从配置文件中读取。
                Integer pageNo = (Integer) paramMap.get("pageNo");
                if (null != pageNo) {
                    Integer pageSize = (Integer) paramMap.get("pageSize");
                    Pageable pageable = new Pageable(pageNo, pageSize);
                    if (paramMap.containsKey("orderBy")) {
                        pageable.setOrderBy((String) paramMap.get("orderBy"));
                    }
                    return pageable;
                }
            }
        }
        return null;
    }

    @Override
    public Object plugin(Object target) {
//		return Plugin.wrap(target, this);
        if (Executor.class.isAssignableFrom(target.getClass())) {
            return Plugin.wrap(target, this);
        }

        return target;
    }

    @Override
    public void setProperties(Properties p) {
        String dialectClass = p.getProperty("dialectClass");

        try {
            setDialect((Dialect) Class.forName(dialectClass).newInstance());
        } catch (Exception e) {
            throw new RuntimeException("cannot create dialect instance by dialectClass:" + dialectClass, e);
        }

    }

    /**
     * 查询总记录数
     *
     * @param sql
     * @param mappedStatement
     * @param boundSql
     * @return
     * @throws SQLException
     */
    private int queryTotal(String sql, MappedStatement mappedStatement,
                           BoundSql boundSql) throws SQLException {

        Connection connection = null;
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        try {

            connection = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection();

            String countSql = this.dialect.getCountString(sql);

            countStmt = connection.prepareStatement(countSql);
            BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());

            setParameters(countStmt, mappedStatement, countBoundSql, boundSql.getParameterObject());

            rs = countStmt.executeQuery();
            int totalCount = 0;
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }

            return totalCount;
        } catch (SQLException e) {
            logger.error("查询总记录数出错", e);
            throw e;
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    logger.error("exception happens when doing: ResultSet.close()", e);
                }
            }

            if (countStmt != null) {
                try {
                    countStmt.close();
                } catch (SQLException e) {
                    logger.error("exception happens when doing: PreparedStatement.close()", e);
                }
            }

            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    logger.error("exception happens when doing: Connection.close()", e);
                }
            }
        }

    }

    /**
     * 对SQL参数(?)设值
     *
     * @param ps
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @throws SQLException
     */
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
                               Object parameterObject) throws SQLException {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);
    }

    public Dialect getDialect() {
        return dialect;
    }

    public void setDialect(Dialect dialect) {
        this.dialect = dialect;
    }

    private MappedStatement copyFromNewSql(MappedStatement ms,
                                           BoundSql boundSql, String sql) {
        BoundSql newBoundSql = copyFromBoundSql(ms, boundSql, sql);
        return copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
    }

    public static class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql,
                                      String sql) {
        BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        return newBoundSql;
    }

    //see: MapperBuilderAssistant
    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuffer keyProperties = new StringBuffer();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }

        //setStatementTimeout()
        builder.timeout(ms.getTimeout());

        //setStatementResultMap()
        builder.parameterMap(ms.getParameterMap());

        //setStatementResultMap()
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());

        //setStatementCache()
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }
}
